Skip to content

DDD 分层架构入门指南

在中大型业务系统中,代码最容易出现的问题不是“功能做不出来”,而是功能越做越多之后,代码越来越难维护。

常见情况包括:

  • Controller 里写了一堆业务判断;
  • Service 里既查数据库,又调用接口,还处理复杂业务规则;
  • DAO、Mapper、缓存、消息队列代码和业务逻辑混在一起;
  • 换一个数据库、改一个接口协议,就牵一发动全身;
  • 单元测试很难写,因为业务逻辑强依赖数据库、Redis、Spring 容器。

领域驱动设计,也就是 DDD,强调围绕业务领域建模。而 DDD 分层架构的核心目标,就是把系统按照职责拆分成不同层次,让业务逻辑和技术实现解耦。

本文会结合一个实际项目模块划分,带你从零理解 DDD 分层架构。


一、什么是 DDD 分层架构? ​

DDD 分层架构是领域驱动设计中非常基础、也非常重要的一种架构模式。

它的核心思想是:

将系统按照职责划分为多个层次,每一层只关注自己的事情,层与层之间通过接口协作,保证领域逻辑不被技术细节污染。

简单来说,DDD 分层架构想解决的问题是:

  • 业务逻辑应该放在哪里?
  • 数据库操作应该放在哪里?
  • HTTP 接口、MQ 消费者、定时任务应该放在哪里?
  • Spring Boot 启动配置应该放在哪里?
  • 公共枚举、异常、返回体应该放在哪里?

如果没有明确的分层,代码就很容易变成“大杂烩”。


二、为什么需要分层? ​

在没有清晰分层的项目中,代码通常会长这样:

java
@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping("/register")
    public Result register(@RequestBody RegisterRequest request) {
        // 参数校验
        // 查询数据库
        // 判断用户是否存在
        // 写入数据库
        // 发送消息
        // 返回结果
    }
}

表面上看,这样写很直接。

但是随着业务复杂度上升,会出现几个严重问题:

1. 业务逻辑和技术实现混在一起 ​

Controller 本来应该只负责接收请求,但现在它还负责:

  • 参数处理;
  • 数据库查询;
  • 业务判断;
  • 消息发送;
  • 返回封装。

这会导致一个类承担太多职责。


2. 代码难以测试 ​

如果业务逻辑直接依赖数据库、Redis、MQ,那么测试业务规则时,就必须准备完整的外部环境。

比如你只是想测试“用户是否允许注册”这个业务规则,却不得不启动 Spring、连接数据库、准备测试数据。


3. 技术变更影响业务代码 ​

如果项目原来使用 MyBatis,后来想换成 JPA,或者从 MySQL 改成其他存储方式,业务代码可能也要跟着改。

这说明业务逻辑已经被技术实现污染了。


4. 项目越大越难维护 ​

小项目中,Controller-Service-Dao 的结构也许够用。

但当业务越来越复杂,Service 层往往会变成一个“万能层”:

  • 写业务逻辑;
  • 查数据库;
  • 调第三方接口;
  • 处理缓存;
  • 发 MQ;
  • 做事务控制。

最终 Service 层越来越臃肿,难以理解和维护。


三、DDD 分层架构的核心原则 ​

DDD 分层架构最重要的原则是:

依赖方向由外向内,领域层处于核心位置。

也就是说:

  • 外层可以依赖内层;
  • 内层不能依赖外层;
  • Domain 层是业务核心;
  • Domain 层不应该依赖数据库、缓存、Web 框架、ORM 框架等技术细节。

可以简单理解为:

text
外部入口层 trigger
        ↓
领域核心层 domain
        ↑
基础设施层 infrastructure

这里看起来有点奇怪:

  • trigger 会调用 domain;
  • infrastructure 也依赖 domain;
  • 但 domain 不直接依赖 infrastructure。

这背后体现的就是依赖倒置原则。


四、项目模块划分示例 ​

假设我们有一个实际项目,模块结构如下:

text
chatgpt-data
├── chatgpt-data-app
├── chatgpt-data-trigger
├── chatgpt-data-domain
├── chatgpt-data-infrastructure
└── chatgpt-data-types

每个模块代表一层职责。

下面逐层讲解。


五、各层职责详解 ​

1. 领域核心层:domain ​

对应模块:

text
chatgpt-data-domain

这是整个系统的核心,也可以说是系统的“心脏”。

Domain 层主要存放业务相关的核心代码,包括:

  • 实体;
  • 值对象;
  • 聚合根;
  • 领域服务;
  • 仓储接口;
  • 业务规则;
  • 领域行为。

Domain 层应该关注什么? ​

Domain 层只关心业务问题,例如:

  • 用户能不能注册?
  • 订单能不能支付?
  • 优惠券是否可用?
  • 账户余额是否足够?
  • 某个业务状态能否流转到下一个状态?

它不应该关心:

  • 数据是存在 MySQL 还是 MongoDB;
  • 缓存用的是 Redis 还是本地缓存;
  • 接口是 HTTP 还是 RPC;
  • 框架是 Spring MVC 还是其他 Web 框架;
  • ORM 是 MyBatis 还是 JPA。

Domain 层通常包含哪些内容? ​

一个典型的 domain 模块可能长这样:

text
chatgpt-data-domain
├── model
│   ├── entity
│   ├── valobj
│   └── aggregate
├── service
└── repository

其中:

目录说明
entity实体,具有唯一标识和生命周期
valobj值对象,描述某种业务概念,无唯一身份
aggregate聚合根,负责维护一组对象的一致性
service领域服务,处理不适合放在实体中的复杂业务逻辑
repository仓储接口,只定义接口,不写具体实现

重点:Domain 层只定义 Repository 接口 ​

例如:

java
public interface UserRepository {

    User findByUserId(String userId);

    void save(User user);
}

这里的 UserRepository 只是一个接口。

Domain 层并不关心它最终是通过 MyBatis 查询数据库,还是通过 Redis、ES、远程接口获取数据。

这就保证了领域层的纯粹性。


2. 基础设施层:infrastructure ​

对应模块:

text
chatgpt-data-infrastructure

Infrastructure 层负责所有技术实现。

它主要处理:

  • 数据库访问;
  • Redis 操作;
  • MQ 发送;
  • 第三方 API 调用;
  • 文件存储;
  • ORM 框架适配;
  • Repository 接口实现。

如果说 domain 层负责“业务抽象”,那么 infrastructure 层负责“技术落地”。


Infrastructure 层通常包含哪些内容? ​

text
chatgpt-data-infrastructure
├── dao
├── mapper
├── repository
├── redis
├── gateway
└── config
目录说明
dao / mapper数据库访问对象,例如 MyBatis Mapper
repository实现 domain 层定义的仓储接口
redisRedis 操作相关代码
gateway第三方接口调用
config基础设施相关配置

Repository 实现示例 ​

Domain 层定义接口:

java
public interface UserRepository {

    User findByUserId(String userId);

    void save(User user);
}

Infrastructure 层提供实现:

java
@Repository
public class UserRepositoryImpl implements UserRepository {

    @Resource
    private UserMapper userMapper;

    @Override
    public User findByUserId(String userId) {
        UserPO userPO = userMapper.selectByUserId(userId);
        return convertToDomain(userPO);
    }

    @Override
    public void save(User user) {
        UserPO userPO = convertToPO(user);
        userMapper.insert(userPO);
    }
}

注意这里的关键点:

接口定义在 domain,具体实现放在 infrastructure。

这样做的好处是:

  • Domain 层不依赖 MyBatis;
  • Domain 层不知道数据库表结构;
  • 更换存储方式时,只需要修改 infrastructure;
  • 核心业务逻辑不会被技术细节影响。

3. 触发器层:trigger ​

对应模块:

text
chatgpt-data-trigger

Trigger 层是系统对外暴露的入口。

常见入口包括:

  • HTTP Controller;
  • MQ Consumer;
  • 定时任务 Job;
  • RPC 接口;
  • WebSocket 入口。

它的职责是接收外部请求,然后调用 domain 或 application 相关服务完成业务处理。


Trigger 层应该做什么? ​

Trigger 层适合处理:

  • 接收请求;
  • 参数转换;
  • 参数基础校验;
  • 调用业务服务;
  • 返回结果封装。

例如:

java
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserDomainService userDomainService;

    @PostMapping("/register")
    public Result register(@RequestBody RegisterRequest request) {
        userDomainService.register(request.getUserId(), request.getUserName());
        return Result.success();
    }
}

Controller 不应该写复杂业务逻辑。

它更像是一个“适配器”,负责把外部请求转换成系统内部能够理解的调用。


Trigger 层不应该做什么? ​

Trigger 层不应该:

  • 直接操作数据库;
  • 编写复杂业务规则;
  • 直接依赖 MyBatis Mapper;
  • 处理大量状态流转逻辑;
  • 把业务判断写在 Controller 里。

错误示例:

java
@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
    UserPO userPO = userMapper.selectByUserId(request.getUserId());

    if (userPO != null) {
        return Result.fail("用户已存在");
    }

    userMapper.insert(...);

    return Result.success();
}

这种写法会让 Controller 变得越来越重,也会让业务规则分散在各个入口中。


4. 应用启动层:app ​

对应模块:

text
chatgpt-data-app

App 层是整个应用的启动入口。

它主要负责:

  • Spring Boot 启动类;
  • 配置文件;
  • Bean 装配;
  • 数据源配置;
  • 中间件配置;
  • 将各个模块组装起来运行。

例如:

text
chatgpt-data-app
├── Application.java
├── config
└── resources
    ├── application.yml
    └── logback-spring.xml

App 层本身一般不承载具体业务逻辑。

它更像是整个系统的“装配中心”。


5. 公共类型定义层:types ​

对应模块:

text
chatgpt-data-types

Types 层用于存放跨模块复用的通用类型。

比如:

  • 通用枚举;
  • 常量定义;
  • 公共异常;
  • 通用返回体;
  • 通用工具类;
  • 基础 DTO;
  • 错误码定义。

例如:

text
chatgpt-data-types
├── enums
├── constants
├── exception
├── response
└── utils

Types 层可以被其他层共同依赖,目的是避免重复定义。

不过需要注意:

Types 层应该只放真正通用的内容,不要把业务逻辑塞进去。

如果所有东西都往 types 里放,types 很容易变成另一个“大杂烩”。


六、各层之间的依赖关系 ​

DDD 分层架构中,依赖方向非常重要。

整体依赖关系可以理解为:

text
trigger         → domain → types
infrastructure → domain → types
app             → trigger
app             → domain
app             → infrastructure
app             → types

也就是说:

  • trigger 依赖 domain;
  • infrastructure 依赖 domain;
  • domain 依赖 types;
  • app 依赖所有模块,用于启动和组装;
  • types 被各层共同依赖。

依赖方向示意图 ​

mermaid
flowchart TD
    Types[types 公共类型层]
    Domain[domain 领域核心层]
    Infrastructure[infrastructure 基础设施层]
    Trigger[trigger 触发器层]
    App[app 应用启动层]

    Domain --> Types
    Infrastructure --> Domain
    Trigger --> Domain
    App --> Trigger
    App --> Domain
    App --> Infrastructure
    App --> Types

这里需要重点理解:

Domain 层永远不应该反向依赖 infrastructure 或 trigger。

也就是说,下面这种依赖是错误的:

text
domain → infrastructure
domain → trigger

因为一旦 domain 依赖了 infrastructure,就意味着业务逻辑开始依赖技术实现了。


七、代码依赖方向与运行时调用方向 ​

DDD 分层中有一个新手很容易困惑的点:

为什么代码依赖方向和运行时调用方向好像是反的?

例如在 Repository 场景中:

代码依赖方向是:

text
infrastructure → domain → types

但是运行时调用方向可能是:

text
trigger → domain → infrastructure

看起来好像 domain 调用了 infrastructure。

实际上,domain 调用的是自己定义的接口,而不是 infrastructure 的具体实现。


依赖倒置的体现 ​

假设 :

java
public interface UserRepository {

    User findByUserId(String userId);
}

infrastructure 层:

java
@Repository
public class UserRepositoryImpl implements UserRepository {

    @Override
    public User findByUserId(String userId) {
        // 通过 MyBatis 查询数据库
    }
}

domain 层中的:

java
public class UserDomainService {

    private final UserRepository userRepository;

    public UserDomainService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String userId) {
        User user = userRepository.findByUserId(userId);

        if (user != null) {
            throw new BizException("用户已存在");
        }

        // 执行业务逻辑
    }
}

这里 UserDomainService 并不知道 UserRepository 的实现类是谁。

真正运行时,由 Spring 容器把 UserRepositoryImpl 注入进来。

这就是依赖倒置原则:

高层模块不依赖低层模块,两者都依赖抽象。

换句话说:

  • Domain 层依赖 Repository 抽象;
  • Infrastructure 层实现 Repository 抽象;
  • Domain 层不关心具体技术实现;
  • 技术细节被隔离在 Infrastructure 层。

八、DDD 分层架构和传统三层架构的区别 ​

很多新手会问:

DDD 分层架构和传统 Controller-Service-Dao 三层架构有什么区别?

传统三层架构一般是:

text
Controller → Service → Dao

这种结构简单清晰,适合很多普通项目。

但是问题在于,Service 层很容易变成“超级大杂烩”。

它既负责业务逻辑,又负责:

  • 调用 Dao;
  • 处理事务;
  • 转换对象;
  • 调用外部接口;
  • 处理缓存;
  • 编排流程。

DDD 分层架构则更强调:

业务逻辑属于 Domain 层,技术实现属于 Infrastructure 层,外部入口属于 Trigger 层。

对比来看:

维度传统三层架构DDD 分层架构
核心层ServiceDomain
业务逻辑位置通常在 Service明确放在 Domain
数据库依赖Service 往往直接或间接依赖 DAODomain 只依赖 Repository 接口
技术隔离较弱较强
适合场景简单 CRUD、中小型系统复杂业务、中大型系统
可测试性一般更好
可维护性项目变复杂后容易下降更适合长期演进

九、代码到底应该放在哪一层? ​

这是学习 DDD 分层架构时最常见的问题。

可以按照下面的规则判断。


1. 如果是业务规则,放 domain ​

例如:

  • 用户是否允许注册;
  • 订单是否可以取消;
  • 优惠券是否满足使用条件;
  • 账户余额是否足够;
  • 某个状态是否可以流转;
  • 积分如何计算;
  • 风控规则如何判断。

这些都属于业务核心,应该放在 domain 层。


2. 如果是技术实现,放 infrastructure ​

例如:

  • SQL 查询;
  • MyBatis Mapper;
  • Redis 读写;
  • MQ 发送;
  • 第三方接口调用;
  • 文件上传;
  • 对象存储;
  • Elasticsearch 查询。

这些都属于技术细节,应该放在 infrastructure 层。


3. 如果是接收外部请求,放 trigger ​

例如:

  • Controller;
  • MQ Consumer;
  • 定时任务;
  • RPC Provider;
  • WebSocket Handler。

这些都是系统入口,应该放在 trigger 层。


4. 如果是应用启动和配置,放 app ​

例如:

  • Spring Boot 启动类;
  • application.yml;
  • Bean 配置;
  • 数据源配置;
  • 扫描路径配置;
  • 中间件配置。

这些都应该放在 app 层。


5. 如果是通用定义,放 types ​

例如:

  • 通用返回对象;
  • 通用异常;
  • 错误码;
  • 公共枚举;
  • 常量;
  • 工具类。

这些可以放在 types 层。


十、一个简单判断口诀 ​

可以记住下面这个口诀:

业务规则放 domain,技术实现放 infrastructure,外部入口放 trigger,启动装配放 app,公共定义放 types。

再具体一点:

text
描述业务本身的代码 → domain
连接数据库、缓存、第三方系统的代码 → infrastructure
接收 HTTP、MQ、定时任务的代码 → trigger
启动类和配置装配 → app
通用枚举、异常、返回体 → types

十一、分层会不会让代码量变多? ​

答案是:会。

DDD 分层架构初期确实会增加一些代码量,例如:

  • Repository 接口;
  • Repository 实现类;
  • DTO 和领域对象转换;
  • PO 和领域对象转换;
  • 模块之间的调用关系;
  • 一些额外的抽象。

对于非常简单的 CRUD 项目,这种分层可能显得有点“重”。

但是当项目规模变大后,分层带来的收益会非常明显。


分层带来的好处 ​

1. 业务逻辑更清晰 ​

核心业务都集中在 domain 层,找业务规则时不用到处翻 Controller、Mapper、Utils。


2. 技术实现更容易替换 ​

如果数据库访问方式变了,只需要改 infrastructure 层。

Domain 层不用动。


3. 更容易单元测试 ​

Domain 层不依赖数据库、Redis、MQ,可以直接通过 mock Repository 接口来测试业务逻辑。


4. 团队协作更清楚 ​

不同层职责明确,团队成员更容易知道代码应该写在哪里。


5. 系统更适合长期演进 ​

业务系统通常不是一次性写完的,而是长期迭代的。

DDD 分层架构可以降低后续需求变更带来的影响范围。


十二、常见误区 ​

误区一:DDD 就是多建几个模块 ​

不是。

DDD 分层架构不是简单地把代码拆成几个 Maven 模块。

真正重要的是:

每一层的职责边界是否清晰,依赖方向是否正确,领域逻辑是否独立。

如果只是把 Controller、Service、Mapper 换个目录名,但业务逻辑仍然和数据库操作混在一起,那并不是真正的 DDD 分层。


误区二:Domain 层可以直接调用 Mapper ​

不可以。

Mapper 是典型的基础设施实现,应该放在 infrastructure 层。

Domain 层如果直接调用 Mapper,就会依赖 MyBatis 这样的技术框架,破坏领域层的纯粹性。


误区三:所有逻辑都放 Domain 层 ​

也不对。

Domain 层放的是业务核心逻辑。

但像下面这些内容不应该放在 domain:

  • HTTP 参数解析;
  • 数据库 SQL;
  • Redis Key 拼接;
  • MQ Topic 配置;
  • 第三方接口请求组装;
  • Spring Bean 配置。

这些都属于外部输入或技术细节。


误区四:Types 层什么都能放 ​

Types 层只适合放通用定义。

不要把业务服务、数据库对象、复杂工具类都塞进去。

否则 types 层会变成一个新的垃圾桶模块。


十三、推荐的模块依赖结构 ​

一个比较清晰的依赖结构可以是:

text
chatgpt-data-app
    depends on:
        chatgpt-data-trigger
        chatgpt-data-domain
        chatgpt-data-infrastructure
        chatgpt-data-types

chatgpt-data-trigger
    depends on:
        chatgpt-data-domain
        chatgpt-data-types

chatgpt-data-infrastructure
    depends on:
        chatgpt-data-domain
        chatgpt-data-types

chatgpt-data-domain
    depends on:
        chatgpt-data-types

chatgpt-data-types
    depends on:
        none

核心原则是:

text
domain 不能依赖 infrastructure
domain 不能依赖 trigger
domain 尽量保持纯粹

十四、总结 ​

DDD 分层架构的核心,不是为了让项目看起来更复杂,而是为了让系统在业务增长后依然保持清晰、稳定、可维护。

它的关键思想可以总结为:

  1. Domain 层是核心
    业务逻辑应该放在领域层,而不是散落在 Controller 或基础设施代码中。

  2. Infrastructure 层负责技术实现
    数据库、缓存、第三方接口、MQ 等技术细节都应该隔离在基础设施层。

  3. Trigger 层负责外部入口
    HTTP、MQ、定时任务等入口只负责接收请求和触发业务流程。

  4. App 层负责启动装配
    它是系统的启动入口,负责把各层组装起来。

  5. Types 层负责通用定义
    公共枚举、异常、常量、返回体等可以放在这里。

  6. 依赖方向必须稳定
    外层依赖内层,领域层不依赖技术实现。

一句话概括:

DDD 分层架构的本质,是让业务逻辑摆脱技术细节的束缚,成为系统真正稳定、可复用、可测试的核心。